Medium 清新閱讀版:連結
今天已經是第鐵人賽第24天了!
在前面的23天,與大家分享了許多撰寫 PHPUnit 測試程式碼所需的知識,之後的文章就讓我們來來模擬一些情境題,並在這些情境題底下,實際去設計測試案例函數吧!
作為第一個情境題,我們就選「網站文章」來當作第一個挑戰吧!
這邊我們假設網站是採前後端分離的設計,因此我們就專注在測試 API 的部分。
依據以上的使用案例,我們可規畫出以下 API:
GET /api/articles
GET /api/articles/{id}
GET /api/articles/{id}/comments
POST /api/articles/comments
接著就來實作 API 吧!
app/Http/Controllers/Api/ApiController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use function response;
class ApiController extends Controller
{
    public function respondJson($data)
    {
        return response()->json([
            'data' => $data,
        ]);
    }
    public function respondNotFound()
    {
        return response()->json('', 404);
    }
}
app/Http/Controllers/Api/ArticleController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Article;
use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ArticleController extends ApiController
{
    public function index(Request $request)
    {
        $articles = Article::all();
        return $this->respondJson($articles);
    }
    public function show($id)
    {
        $article = Article::find($id);
        if (empty($article)) {
            return $this->respondNotFound();
        }
        return $this->respondJson($article);
    }
    public function comments(Request $request, $id)
    {
        $article = Article::find($id);
        if (empty($article)) {
            return $this->respondNotFound();
        }
        $comments = $article->comments;
        return $this->respondJson($comments);
    }
    public function storeComment(Request $request, $id)
    {
        $user = Auth::user();
        $article = Article::find($id);
        if (empty($article)) {
            return $this->respondNotFound();
        }
        $data = [
            'article_id' => $article->id,
            'content' => $request->input('comment'),
            'user_id' => $user->id,
        ];
        $comment = new Comment($data);
        $comment->save();
        return $this->respondJson($comment);
    }
}
app/Models/Article.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
    use HasFactory;
    protected $fillable = [
        'content',
    ];
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
app/Models/Comment.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
    use HasFactory;
    protected $fillable = [
        'content',
        'user_id',
        'article_id',
    ];
    public function article()
    {
        return $this->belongsTo(Article::class);
    }
}
app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];
    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\ArticleController;
Route::prefix('articles')->group(function() {
    Route::get('', [ArticleController::class, 'index'])
        ->name('article.list');
    Route::get('/{id}', [ArticleController::class, 'show'])
        ->where('id', '[0-9]+')
        ->name('article.one');
    Route::get('/{id}/comments', [ArticleController::class, 'comments'])
        ->where('id', '[0-9]+')
        ->name('article.one.comments');
    Route::post('/{id}/comments', [ArticleController::class, 'storeComment'])
        ->middleware('auth:api')
        ->where('id', '[0-9]+')
        ->name('article.one.comments.store');
});
database/migrations/2014_10_12_000000_create_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
};
database/migrations/2022_10_02_174939_create_articles_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->text('content');
            $table->integer('page_views');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
};
database/migrations/2022_10_08_172525_create_comments_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->integer('user_id');
            $table->integer('article_id');
            $table->text('content');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comments');
    }
};
這邊我們要準備的是各 Model 的 Factory 類別,以及批次產生測試資料的 Seeders:
User Factory
<?php
namespace Database\Factories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
    /**
    * Define the model's default state.
    *
    * @return array
    */
    public function definition(): array
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->safeEmail,
            'email_verified_at' => $this->faker->dateTime(),
            'password' => bcrypt($this->faker->password),
            'remember_token' => Str::random(10)
        ];
    }
}
Article Factory
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
 */
class ArticleFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'content' => $this->faker->text,
            'page_views' => 0,
        ];
    }
}
Comment Factory
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment>
 */
class CommentFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'content' => $this->faker->text,
        ];
    }
}
User Seeder
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\UserLog;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        User::factory()
            ->count(10)
            ->create();
    }
}
Article Seeder
<?php
namespace Database\Seeders;
use App\Models\Article;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Database\Seeder;
class ArticleSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $articles = Article::factory()
            ->count(10)
            ->create();
        $users = User::all();
        foreach ($articles as $article) {
            $commentCount = random_int(1, 5);
            for ($i = 0; $i < $commentCount; $i ++) {
                $user = $users->random();
                Comment::factory()
                    ->create([
                        'user_id' => $user->id,
                        'article_id' => $article->id,
                    ]);
            }
        }
    }
}
到這邊為止,我們已經把測試目標準備好了,明天我們就來針對各使用案例來寫測試吧!